SPB Git forge

spb/countryatlas

Public
20commits 1branches 0releases
268.3 MBsize
maindefault branch
13 days agolast push
TypeScript 57% Python 38.6% JavaScript 3.6% CSS 0.6%
3.5 KB · 63 lines tsx
Raw Blame History
1import { ImageResponse } from 'next/og';2import { t } from '@/i18n';3import { safe } from '@/lib/api';4import { apiExplore } from '@/lib/api-explore';5import { formatValue } from '@/lib/format';6import { OG_INK2, OG_INK3, OG_RULE, OG_SIZE_H, OG_SIZE_W, OgFrame, OgWordmark } from '../../og-shared';78export const alt = 'Indicator by country on CountryAtlas';9export const size = { width: OG_SIZE_W, height: OG_SIZE_H };10export const contentType = 'image/png';11export const revalidate = 3600;1213/** Light→dark sequential ramp (globals.css --seq-1…7, light theme) — Satori needs literal colours. */14const SEQ = ['#cde2fb', '#9ec5f4', '#6da7ec', '#3987e5', '#256abf', '#184f95', '#0d366b'];1516/** Per-indicator social image: name, world value + kind, year, coverage, mini sequential legend. */17export default async function IndicatorOgImage({ params }: { params: Promise<{ slug: string }> }) {18  const { slug } = await params;19  const data = await safe(apiExplore.indicator(slug));20  const ind = data?.indicator;21  const name = ind?.name ?? slug.replace(/-/g, ' ');22  const wl = data?.world_latest ?? null;23  const kind = wl ? (wl.kind === 'sum' ? 'World total' : wl.kind === 'weighted_mean' ? 'World average' : 'World median') : null;24  const sub = [ind?.unit, ind?.topic ? ind.topic.replace(/-/g, ' ') : null].filter(Boolean).join(' · ');25  const coverage = data ? `${data.coverage.n_countries ?? 0} countries · ${data.years.first ?? ''}–${data.years.last_actual ?? ''}` : '';26  return new ImageResponse(27    (28      <OgFrame>29        <OgWordmark size={28} />30        <div style={{ display: 'flex', flexDirection: 'column', marginTop: 48, maxWidth: 900 }}>31          <div style={{ fontSize: 22, color: OG_INK3, textTransform: 'uppercase', letterSpacing: 2, display: 'flex' }}>{t('common.indicator')}</div>32          <div style={{ fontSize: name.length > 40 ? 52 : 64, fontWeight: 600, letterSpacing: -1.5, lineHeight: 1.05, marginTop: 8 }}>{name}</div>33          {sub ? <div style={{ marginTop: 10, fontSize: 24, color: OG_INK2 }}>{sub}</div> : null}34        </div>35        <div style={{ display: 'flex', alignItems: 'flex-end', justifyContent: 'space-between', marginTop: 'auto', paddingTop: 24, borderTop: `1px solid ${OG_RULE}` }}>36          {wl && ind && wl.value != null ? (37            <div style={{ display: 'flex', flexDirection: 'column' }}>38              <div style={{ fontSize: 18, color: OG_INK3, textTransform: 'uppercase', letterSpacing: 1.5 }}>{kind}</div>39              <div style={{ fontSize: 52, fontWeight: 600, marginTop: 6 }}>{formatValue(wl.value, ind)}</div>40              <div style={{ fontSize: 18, color: OG_INK3, marginTop: 2 }}>{`${wl.year ?? ''}${coverage ? ` · ${coverage}` : ''}`}</div>41            </div>42          ) : (43            <div style={{ fontSize: 24, color: OG_INK2 }}>{coverage || 'World map, trend, ranking and sources'}</div>44          )}45          <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end' }}>46            <div style={{ display: 'flex', gap: 3 }}>47              {SEQ.map((c) => (48                <div key={c} style={{ width: 34, height: 14, background: c, borderRadius: 2 }} />49              ))}50            </div>51            <div style={{ display: 'flex', justifyContent: 'space-between', width: 256, marginTop: 6, fontSize: 16, color: OG_INK3 }}>52              <span>low</span>53              <span>high</span>54            </div>55          </div>56        </div>57        <div style={{ position: 'absolute', right: 64, bottom: 24, fontSize: 20, color: OG_INK3 }}>{t('og.site')}</div>58      </OgFrame>59    ),60    { ...size },61  );62}63